S03-09 JS-高级-对象增强
[TOC]
对象增强@
属性描述符
在前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的:
但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过 delete 删除的?这个属性是否在 for-in 遍历的时候被遍历出来呢?
var obj = {
name: 'why',
age: 18,
height: 1.88
}
如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符。通过属性描述符可以精准的添加或修改对象的属性;属性描述符需要使用 Object.defineProperty() 来对属性进行添加或者修改;
属性描述符(Property Descriptor):是用于精确控制对象属性行为的配置对象。通过定义属性是否可写、可枚举、可配置,以及如何通过 getter
和 setter
访问属性值,开发者可以实现对对象属性的精细化控制。
属性描述符的类型有两种:
数据描述符(Data Descriptor)
存取描述符(Accessor Descriptor)
属性描述符的默认值与行为:
默认值规则
显式声明:通过
defineProperty
定义的属性, 若未指定描述符属性,其默认值为false
或undefined
。js// 默认值:writable: false, enumerable: false, configurable: false Object.defineProperty(obj, "x", { value: 10 });
对象字面量:通过
{}
定义的属性默认enumerable: true
。
互斥规则:数据描述符与存取描述符不能混用。
js// 错误示例:同时设置 value 和 get Object.defineProperty(obj, "prop", { value: 42, get() { return this.value; } // TypeError });
不可逆操作:
configurable: false
后,除writable
可从true
改为false
外,其他描述符属性均不可修改。jsObject.defineProperty(obj, "key", { configurable: false }); Object.defineProperty(obj, "key", { enumerable: true }); // TypeError
常见应用场景:
数据劫持(响应式系统):Vue 2.x 使用 Object.defineProperty() 实现数据双向绑定。
jsfunction observe(obj, key, callback) { let value = obj[key]; Object.defineProperty(obj, key, { get() { return value; }, set(newVal) { if (value !== newVal) { value = newVal; callback(newVal); } } }); }
创建不可变属性:防止关键属性被意外修改或删除。
jsconst config = {}; Object.defineProperty(config, "apiUrl", { value: "https://api.example.com", writable: false, configurable: false });
隐藏内部状态:封装对象内部数据,仅暴露受控接口。
jsclass Counter { constructor() { let count = 0; Object.defineProperty(this, "count", { get: () => count, set: (value) => { throw new Error("count 不可直接修改"); } }); this.increment = () => count++; } }
注意事项:
严格模式下的行为:在严格模式(
'use strict'
)下,违反描述符规则的操作会抛出错误。js'use strict'; const obj = {}; Object.defineProperty(obj, "x", { value: 10 }); obj.x = 20; // TypeError: Cannot assign to read-only property 'x'
性能影响:频繁使用 Object.defineProperty() 或动态修改描述符可能影响性能,需谨慎使用。
兼容性:所有现代浏览器均支持属性描述符,但在老旧环境中需注意 Polyfill。
数据描述符
数据描述符(Data Descriptor):通过 value
和 writable
直接定义属性的值及其可写性。
数据数据描述符有如下四个特性:
configurable
configurable:boolean
,是否可删除/修改,表示属性是否可以通过 delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]默认为 true;
当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为 false;
用法:
▸ 不可删除
▸ 不可重新配置:除 writable 可从 true 改为 false 外,其他描述符属性均不可修改。
▸ 通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为 false
enumerable
enumerable:是否可遍历,表示属性是否可以通过 for...in
或者 Object.keys()
遍历该属性;
当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为 true;
当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为 false;
writable
writable:是否可修改属性值;
当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为 true;
当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为 false;
value
value:属性的 value 值,读取属性时会返回该值,修改属性时,会对其进行修改;
- 默认情况下这个值是 undefined;
示例:
1、通过对象字面量{}
定义的变量,默认是可删除、可遍历、可修改的。
2、通过 Object.defineProperty() 定义的变量,默认是不可删除、不可遍历、不可修改的。
3、当属性描述符为false时的对象操作行为。
存取描述符
存取描述符(Accessor Descriptor):通过 get
和 set
函数控制属性的访问和赋值,适用于需要动态计算属性值或拦截赋值操作的场景。
存取属性描述符有如下四个特性:
configurable:同数据描述符。
enumerable:同数据描述符。
get:获取属性时会执行的函数。默认为 undefined
set:设置属性时会执行的函数。默认为 undefined
示例:
const user = {
_name: "Alice"
};
Object.defineProperty(user, "name", {
get() { return this._name; },
set(newName) { this._name = newName.toUpperCase(); },
enumerable: true
});
user.name = "bob";
console.log(user.name); // "BOB"
常见对象方法
defineProperty
Object.defineProperty():(obj, prop, descriptor)
,用于精确控制对象属性行为的核心方法。允许开发者直接在一个对象上定义新属性或修改现有属性,并通过属性描述符精确配置属性的读写性、可枚举性、可配置性等特性。
defineProperties
Object.defineProperties():(obj, props)
,用于批量定义或修改对象属性的方法,允许通过一次调用配置多个属性的特性(如读写性、可枚举性等)。
getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor:(obj, prop)
,用于获取对象自身属性的描述符的方法。
示例:
getOwnPropertyDescriptors
Object.getOwnPropertyDescriptors():(obj)
,用于获取对象所有自身属性的描述符的方法。是 getOwnPropertyDescriptor 的批量版,适用于需要全面分析或复制对象属性的场景(如深度克隆、元编程)。
示例:
preventExtensions
Object.preventExtensions():(obj)
,用于阻止对象扩展的方法,使目标对象无法添加新的属性。它属于对象的不可变性操作之一,与 Object.seal()
和 Object.freeze()
形成层级递进的限制。
示例:
seal
Object.seal():(obj)
,用于封闭对象的方法。核心作用是:禁止添加新属性,标记所有现有属性为不可配置,允许修改现有属性的值。它属于对象的不可变性操作之一,与 Object.preventExtensions()
和 Object.freeze()
形成递进限制。
示例:
freeze
Object.freeze():(obj)
,用于冻结对象的方法,其核心作用是禁止新增属性,禁止删除属性,禁止修改属性值,禁止修改属性描述符。它是对象不可变性操作的最高限制级别,与 Object.preventExtensions()
和 Object.seal()
形成递进关系。
示例:
原型
对象和函数的原型
对象的原型
JavaScript 当中每个对象都有一个特殊的内置属性 [[Prototype]],这个特殊的对象可以指向另外一个对象。
那么这个对象有什么作用呢?
当我们通过引用对象的属性 key 来获取一个 value 时,它会触发 [[Get]] 的操作;
这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它;
如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;
那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
- 答案是有的,只要是对象都会有这样的一个内置属性;
获取对象原型的方式有两种:
方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);
方式二:通过 Object.getPrototypeOf() 方法可以获取到;
示例:
1、获取对象的原型
2、对象属性查找顺序
函数的原型
那么我们知道上面的东西对于我们的构造函数创建对象来说有什么作用呢?
- 它的意义是非常重大的,接下来我们继续来探讨;
1、将函数看做一个普通的对象,它具备 __proto__ (隐式原型)属性
作用:查找 key 对应的 value 时,会找到原型身上
2、将函数看做一个函数时,它具备prototype (显式原型)属性(注意:不是__proto__
或[[Prototype]]
)
作用:当通过 new 创建对象实例时,对象实例的隐式原型会指向这个构造函数的显式原型:foo.__proto__ = Foo.prototype
注意: 箭头函数没有原型prototype
你可能会问题,老师是不是因为函数是一个对象,所以它有 prototype 的属性呢?
不是的,因为它是一个函数,才有了这个特殊的属性;
而不是它是一个对象,所以有这个特殊的属性;
new、constructor
new 操作原型赋值
我们前面讲过 new 关键字的步骤如下:
1、在内存中创建一个新的对象(空对象);
2、这个对象内部的 [[prototype]]属性 会被赋值为该构造函数的 prototype 属性;
那么也就意味着我们通过 Person 构造函数创建出来的所有对象的[[prototype]]属性都指向 Person.prototype:
constructor 属性
object.constructor:构造函数
,指向创建该对象的构造函数。它通常用于获取对象类型或创建同类新对象,但在某些场景下需要谨慎使用(如原型继承时可能被覆盖)。
事实上原型对象上面是有一个属性的:constructor
- 默认情况下原型上都会添加一个属性叫做 constructor,这个 constructor 指向当前的函数对象;
实例方法-构造函数和原型结合
我们在上一个构造函数的方式创建对象时,有一个弊端:会创建出重复的函数,比如 running、eating 这些函数
那么有没有办法让所有的对象去共享这些函数呢?
可以,将这些函数放到 Person.prototype 的对象上即可;
分析:
- 1、p1 的隐式原型是 Person.prototype 对象
- 2、p1.running 查找规则:
- 先在自己身上查找,没有找到
- 再去原型上查找,找到了
作用:
当多个对象拥有共同的值时,可以将该值放到构造函数的对象的显式原型上;由构造函数创建出来的所有对象,都会共享这些方法
内存图
创建实例对象
function Person(name, age) {
this.name = name;
this.age = age;
}
var p1 = new Person("mr", 18);
var p2 = new Person("tom", 20);
添加原型属性
function Person(name, age) {
this.name = name
this.age = age
}
var p1 = new Person("mr", 18)
var p2 = new Person("tom", 20)
// 添加原型属性
+ Person.prototype.message = "中国"
+ p1.__proto__.info = "中国很美丽"
添加原型方法
function Person(name, age) {
this.name = name
this.age = age
}
var p1 = new Person("mr", 18)
var p2 = new Person("tom", 20)
Person.prototype.message = "中国"
p1.__proto__.info = "中国很美丽"
// 添加原型方法
+ Person.prototype.running = function() {}
新增实例属性
function Person(name, age) {
this.name = name
this.age = age
}
var p1 = new Person("mr", 18)
var p2 = new Person("tom", 20)
Person.prototype.message = "中国"
p1.__proto__.info = "中国很美丽"
// 修改p1.message,p2.message是否改变
+ p1.message = "美国"
+ console.log(p2.message) // => "中国"
重写原型对象
在原有的原型对象上添加新的属性
如果我们需要在原型上添加过多的属性,通常我们会重写整个原型对象
前面我们说过, 每创建一个函数, 就会同时创建它的 prototype 对象, 这个对象也会自动获取 constructor 属性;
而我们这里相当于给 prototype 重新赋值了一个对象, 那么这个新对象的 constructor 属性, 会指向 Object 构造函数, 而不是Person 构造函数
手动添加 constructor
如果希望 constructor 指向 Person,那么可以手动添加 constructor:
上面的方式虽然可以, 但是也会造成 constructor 的[[Enumerable]]特性被设置了 true
默认情况下, 原生的 constructor 属性是不可枚举的
如果希望解决这个问题, 就可以使用我们前面介绍的 Object.defineProperty()函数了
1、手动添加 constructor,指向 Person
2、通过 defineProperty 设置 constructor 属性为不可枚举
构造函数的类方法
添加在构造函数本身的方法,叫类方法
类方法可以在没有实例对象的情况下,调用函数
继承
继承
面向对象的三大特性:封装、继承、多态
封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
多态:不同的对象在执行时表现出不同的形态;
那么这里我们核心讲继承。
那么继承是做什么呢?
继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;
在很多编程语言中,继承是多态的前提;
那么 JavaScript 当中如何实现继承呢?
不着急,我们先来看一下 JavaScript 原型链的机制;
再利用原型链的机制实现一下继承;
示例:
Student 类
Teacher 类
将共同的属性和方法抽取到父类中
原型链
JS 原型链
在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。
我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:
const obj = {
name: "mr",
age: 18,
};
obj.__proto__ = {
// message: 'hello aaa'
};
obj.__proto__ = {
// message: 'hello bbb'
};
obj.__proto__.__proto__ = {
message: "hello ccc",
};
原型链查找顺序图:
const obj = {}
相当于 const obj = new Object()
,所以 obj 是有原型对象的,它的原型对象就是 Object 对象
Object 的原型
最顶层原型:[Object: null prototype] {}
那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型__proto__属性呢?
我们会发现它打印的是 [Object: null prototype] {}
事实上这个原型就是我们最顶层的原型了
从 Object 直接创建出来的对象的原型都是 [Object: null prototype] {}。
那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?
特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;
特殊二:该对象上有很多默认的属性和方法;
内存图-创建 Object 对象
内存图-原型链关系
Object 是所有类的父类
从我们上面的 Object 原型我们可以得出一个结论:原型链最顶层的原型对象就是 Object 的原型对象
实现继承-原型链-继承方法
通过原型链实现继承
如果我们现在需要实现继承,那么就可以利用原型链来实现了:
目前 stu 的原型是 p 对象,而 p 对象的原型是 Person 默认的原型,里面包含 running 等函数;
注意:步骤 3 和步骤 4 不可以调整顺序,否则会有问题
定义父类 Person
///1、定义父类构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {};
Person.prototype.eating = function () {};
实现继承: 创建一个父类的实例对象new Person()
, 用这个实例对象作为子类的原型对象
///2、定义子类构造函数
function Student(sno) {
this.sno = sno
}
// 3、创建父类实例,用它作为子类的原型对象
+ const p = new Person("mr", 18)
+ Student.prototype = p
// 4、为子类添加原型方法
Student.prototype.studying = function() {}
错误的实现继承做法:父类的原型直接赋值给子类的原型
问题:父类和子类共享同一个原型对象,修改了任意一个,另外一个也被修改了
原型链继承的弊端
但是目前有一个很大的弊端:某些属性其实是保存在 p 对象上的;
第一,我们通过直接打印子类实例对象是看不到这个属性的;
第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;
第三,不能给 Person 传递参数(让每个 stu 有自己的属性),因为这个对象是一次性创建的(没办法定制化);
实现继承-借用构造函数-继承属性
借用构造函数继承
为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之为经典继承或者称之为伪造对象):
- steal 是偷窃、剽窃的意思,但是这里可以翻译成借用;
借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数
- 因为函数可以在任意的时刻被调用;
- 因此通过 apply()和 call()方法也可以在新创建的对象上执行构造函数;
组合借用继承的问题
组合继承是 JavaScript 最常用的继承模式之一:
如果你理解到这里, 点到为止, 那么组合来实现继承只能说问题不大;
但是它依然不是很完美,但是基本已经没有问题了;
组合继承存在什么问题呢?
组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数。
- 一次在创建子类原型的时候;
- 另一次在子类构造函数内部(也就是每次创建子类实例的时候);
另外,如果你仔细按照我的流程走了上面的每一个步骤,你会发现:所有的子类实例事实上会拥有两份父类的属性
- 一份在当前的实例自己里面(也就是 stu 本身的),另一份在子类对应的原型对象中(也就是 stu.__proto__里面);
- 当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;
实现继承-寄生组合
原型式继承函数
原型式继承的渊源:
这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON 的创立者)在 2006 年写的一篇文章说起:Prototypal Inheritance in JavaScript(在 JavaScript 中使用原型式继承)
在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的.
为了理解这种方式,我们先再次回顾一下 JavaScript 想实现继承的目的:重复利用另外一个对象的属性和方法.
最终的目的:student 对象的原型指向了 person 对象;
创建中间原型对象的方法:
方法一: 创建父类实例,用它作为子类的原型对象
方法二: 创建空对象,该对象的隐式原型指向父类的原型对象,同时子类的原型对象指向该对象
方法三:
方法四:
封装 1:
封装 2: 寄生组合式继承(最终方案):考虑兼容问题
寄生式继承函数
寄生式继承(Parasitic):
寄生式继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;
寄生式继承的思路是结合原型类继承和工厂模式的一种方式;
即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;
寄生组合式继承
现在我们来回顾一下之前提出的比较理想的组合继承
组合继承是比较理想的继承方式, 但是存在两个问题:
问题一: 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候.
问题二: 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中.
事实上, 我们现在可以利用寄生式继承将这两个问题给解决掉
你需要先明确一点: 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候, 就会将父类型中的属性和方法复制一份到了子类型中. 所以父类型本身里面的内容, 我们不再需要.
这个时候, 我们还需要获取到一份父类型的原型对象中的属性和方法.
能不能直接让子类型的原型对象 = 父类型的原型对象呢?
不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改.
我们使用前面的寄生式思想就可以了.
寄生组合继承的代码
终极方案:寄生组合式继承@
使用到的知识点:原型链、借用构造函数、原型式继承(对象之间)、寄生式函数
1、寄生组合式继承
2、使用寄生组合式继承实现继承
3、打印结果
4、内存图
对象判断方法补充
对象 VS 属性:
object.hasOwnProperty(prop):
返回:boolean
,对象是否有某一个只属于自己的属性(不是在原型上的属性)- 参数
- prop:``,要测试的属性的字符串名称或者 Symbol。
- 返回值
- 如果对象有指定属性作为自有属性,则返回
true
;否则返回false
。
in:
返回:
,判断某个属性是否在某个对象自己或者对象的原型链上for...in:
返回:
,遍历某个对象自己上或者其原型链上所有可枚举的属性(除 Symbol 外)对象 VS 构造函数:
instanceof:
返回:
,用于检测构造函数(Person、Student 类)的 pototype,是否出现在某个实例对象的原型链上对象 VS 对象:
object.isPrototypeOf(obj):
返回:boolean
,用于检测某个对象,是否出现在某个实例对象的原型链上- 参数
- obj:``,要搜索其原型链的对象。
示例:
1、hasOwnProperty
2、in 操作符
3、for...in 操作符
4、instanceof
5、isPrototypeOf
原型继承关系图@
内存图
实现继承-ES6
class 类-定义类
我们会发现,按照前面的构造函数形式创建 类,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。
在ES6(ECMAScript2015)新的标准中使用了 class 关键字来直接定义类;
但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已;
所以学好了前面的构造函数、原型链更有利于我们理解类的概念和继承关系;
那么,如何使用 class 来定义一个类呢?
- 可以使用两种方式来声明类:类声明和类表达式;
class 类-构造函数
如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?
每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor;
当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;
每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;
当我们通过new关键字操作类的时候,会调用这个 constructor 函数,并且执行如下操作:
1、在内存中创建一个新的对象(空对象);
2、这个对象内部的[[prototype]]属性会被赋值为该类的 prototype 属性;
3、构造函数内部的 this,会指向创建出来的新对象;
4、执行构造函数的内部代码(函数体代码);
5、如果构造函数没有返回非空对象,则返回创建出来的新对象;
class 类-实例方法
在上面我们定义的属性都是直接放到了 this 上,也就意味着它是放到了创建出来的新对象中:
在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享;
这个时候我们可以直接在类中定义实例方法;
类中定义多个方法,不需要用
,
分割
class 类-访问器方法@
我们之前讲对象的属性描述符时有讲过对象可以添加setter和getter函数的,那么类也是可以的
class 类-静态方法@
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static 关键字来定义:
class 类和构造函数的异同
我们来研究一下类的一些特性:
- 你会发现它和我们的构造函数的特性其实是一致的;
1、构造函数定义的类
2、class 定义的类
3、相同点
4、不同点
构造函数可以当做普通的函数调用,而 class 类不能
ES6 类的继承-extends
前面我们花了很大的篇幅讨论了在 ES5 中实现继承的方案,虽然最终实现了相对满意的继承机制,但是过程却依然是非常繁琐的。
- 在 ES6 中新增了使用extends 关键字,可以方便的帮助我们实现继承:
ES6 类的继承-super
class 为我们的方法中提供了super关键字
- 执行*super.method(...)*来调用一个父类方法
- 执行*super(...)*来调用一个父类 constructor(只能在子类的 constructor 中执行 super)
我们会发现在上面的代码中我使用了一个 super 关键字,这个 super 关键字有不同的使用方式:
注意:在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过 super 调用父类的构造函数!
super 的使用位置有三个:子类的构造方法、实例方法、静态方法;
示例:
1、在子类的构造方法中使用 super
2、在子类的实例方法中使用 super(方法重写)
3、在子类的静态方法中使用 super(方法重写)
继承内置类
我们也可以让我们的类继承自内置类,比如 Array:
类的混入-mixin@
JavaScript 的类只支持单继承:也就是只能有一个父类
那么在开发中我们我们需要在一个类中添加更多相似的功能时,应该如何来做呢?
这个时候我们可以使用混入(mixin);
应用:React 中的高阶组件
重写内置类@
重写-valueOf()
obj.valueOf():()
,用于将对象转换为原始类型值,通常是在进行数值运算时自动触发。是一个可重写的方法,可以在自定义对象中重写它以返回自定义值。
- 返回:
- value:``,默认返回对象本身,但也可以在自定义对象中重写它以返回自定义值。
- 对于
Object
对象,返回对象本身。 - 对于
Number
对象,返回相应的数字值。 - 对于
String
对象,返回相应的字符串。 - 可以在自定义对象中重写它以返回自定义值。
- 对于
用法:重写valueOf()
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 重写 valueOf 方法
valueOf() {
return this.age; // 返回 age 属性,作为原始值
}
}
const person1 = new Person('Alice', 30);
const person2 = new Person('Tom', 18);
const person3 = new Person('Lucy', 18);
console.log(person1 + 10); // 输出 40,因为 person1.valueOf() 返回 30,30 + 10 = 40
console.log(person1 > person2); // true
console.log(person2 === person3); // false,===只有在2个对象是同一个对象时才为true,只有值相等不行
重写-toString()
toString():()
,将对象转换为字符串表示。这个方法在对象参与字符串拼接或打印时自动调用。是一个可重写的方法,可以在自定义对象中重写它来定义对象的字符串表示方式。
- 返回:
- value:``,默认返回对象的字符串表示形式。
- 对于
Object
对象,返回格式为[object Type]
的字符串。Type
是对象的类型(Object
、Array
、Function
等)。 - 对于
Number
、String
、Date
对象,返回其更有意义的字符串表示。 - 可以在自定义对象中重写它以返回一个更有意义的字符串。
- 对于
用法:重写toString()
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 重写 toString 方法
toString() {
return `${this.name} is ${this.age} years old`;
}
}
const person1 = new Person('Alice', 30);
console.log(person1.toString()); // 输出 "Alice is 30 years old"
Babel
babel-ES6 转 ES5(源码)
1、简单类 Person 转 ES5
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {}
eating() {}
static radomPerson() {}
}
const p1 = new Person("tom", 18);
2、继承类 Person 转 ES5
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {}
eating() {}
static radomPerson() {}
}
class Student extends Person {
constructor(name, age, sno, score) {
super(name, age);
this.sno = sno;
this.score = score;
}
studyding() {}
static radomStudent() {}
}
const stu1 = new Student("mr", 18, 110, 100);
ES5 代码
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});
Object.defineProperty(subClass, "prototype", { writable: false });
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf
? Object.setPrototypeOf.bind()
: function _setPrototypeOf(o, p) {
o.__proto__ = p; // Student.__proto__ = Person
return o;
};
return _setPrototypeOf(o, p);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
} else if (call !== void 0) {
throw new TypeError(
"Derived constructors may only return object or undefined"
);
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function () {})
);
return true;
} catch (e) {
return false;
}
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf.bind()
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _typeof(obj) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (obj) {
return typeof obj;
}
: function (obj) {
return obj &&
"function" == typeof Symbol &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? "symbol"
: typeof obj;
}),
_typeof(obj)
);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", { writable: false });
return Constructor;
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
var Person = /*#__PURE__*/ (function () {
function Person(name, age) {
_classCallCheck(this, Person);
this.name = name;
this.age = age;
}
_createClass(
Person,
[
{
key: "running",
value: function running() {},
},
{
key: "eating",
value: function eating() {},
},
],
[
{
key: "radomPerson",
value: function radomPerson() {},
},
]
);
return Person;
})();
var Student = /*#__PURE__*/ (function (_Person) {
_inherits(Student, _Person);
var _super = _createSuper(Student);
function Student(name, age, sno, score) {
var _this;
_classCallCheck(this, Student);
_this = _super.call(this, name, age);
_this.sno = sno;
_this.score = score;
return _this;
}
_createClass(
Student,
[
{
key: "studyding",
value: function studyding() {},
},
],
[
{
key: "radomStudent",
value: function radomStudent() {},
},
]
);
return Student;
})(Person);
var stu1 = new Student("mr", 18, 110, 100);
多态
JavaScript 中的多态
面向对象的三大特性:封装、继承、多态。
- 前面两个我们都已经详细解析过了,接下来我们讨论一下 JavaScript 的多态。
JavaScript 有多态吗?
维基百科对多态的定义:多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。
非常的抽象,个人的总结:不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现。
那么从上面的定义来看,JavaScript是一定存在多态的。
1、JS中的多态
2、严格语言中的多态条件:
1、必须有继承或接口
2、必须有父类引用指向子类对象
对象字面量增强
1、属性的简写
2、方法的简写
3、计算属性名
解构
1、数组的解构
- 基本使用
- 顺序问题:有严格的顺序
- 解构出数组
- 解构的默认值
2、对象的解构
- 基本使用
- 顺序问题:对象的解构没有顺序,是根据key来解构的
- 重命名变量
- 默认值
- 对象的剩余内容
3、解构的应用